package utils

import (
	"errors"
	"gitee.com/yijimu/gin-frame/utils/crypto"
	"github.com/dgrijalva/jwt-go"
	"github.com/go-redsync/redsync/v4"
	"strconv"
	"time"
)

const tokenBlacklistGracePeriod int64 = 15 //黑名单宽限时间（秒）,设置一个黑名单宽限时间，目的就是避免并发请求中，刷新了token导致部分请求失败的情况

// GenToken 生成 Token
func GenToken(guardName, secret string, params []byte, ttl int64) (tokenStr string, token *jwt.Token, err error) {
	token = jwt.NewWithClaims(
		jwt.SigningMethodHS256,
		customClaims{
			Params: string(params),
			StandardClaims: jwt.StandardClaims{
				ExpiresAt: time.Now().Unix() + ttl, //token 有效期（秒）
				Issuer:    guardName,               // 用于在中间件中区分不同客户端颁发的 token，避免 token 跨端使用
				NotBefore: time.Now().Unix() - 1000,
			},
		},
	)

	tokenStr, err = token.SignedString([]byte(secret))

	return
}

// ValidToken 验证 Token isMustLogin是否必须强制登录，如果为false，表示可以不用登录，如果有token就获取下用户信息
func ValidToken(redisSync *RedsyncClient, guardName, tokenStr, secret string, isMustLogin bool, ttl int64, refreshTokenPeriod int64) (validTokenReturn, error) {
	claims := customClaims{}
	token, _ := jwt.ParseWithClaims(tokenStr, &claims, func(token *jwt.Token) (interface{}, error) {
		return []byte(secret), nil
	})

	returnData := validTokenReturn{}

	if token != nil && token.Valid { //token验证通过
		if claims.Issuer != guardName { // Token 发布者校验
			return returnData, errors.New("发布者不匹配~")
		}

		if IsInBlacklist(tokenStr) {
			if isMustLogin {
				return returnData, errors.New("请登录~")
			}
			return returnData, nil
		}

		returnData.Params = claims.Params
		returnData.Token = token

		// token 续签
		// RefreshGracePeriod  token自动刷新宽限时间（秒）
		if (claims.ExpiresAt - time.Now().Unix()) < refreshTokenPeriod {
			if JoinBlackList(redisSync, token, false) == nil {
				if newTokenStr, newToken, err := GenToken(guardName, secret, []byte(claims.Params), ttl); err == nil {
					returnData.Token = newToken
					returnData.NewTokenStr = newTokenStr
					return returnData, nil
				}
			}
		}

		return returnData, nil
	} else {
		if isMustLogin { //必须要通过登录才能进行下一步
			return returnData, errors.New("请登录~")
		}
		return returnData, nil
	}
}

// JoinBlackList 将token加入黑名单，比如用户退出登录时候，让该token失效; 又比如 token快到期时候，将老的token加入黑名单
// isLogout 是否是 进行用户退出 操作
func JoinBlackList(redisSync *RedsyncClient, token *jwt.Token, isLogout bool) error {
	//思路：
	//	由于 JWT 是无状态的，只能等到它的有效期过了才会失效，服务端无法主动让一个 token 失效
	//	为了解决这个问题，这里使用黑名单策略来解决 JWT 的注销问题，简单来说就将用户主动注销的 token 加入黑名单策略的Redis中
	//	并且必须设置有效期，否则将导致Redis巨大的问题，然后在 Jwt 中间件鉴权时判断 token 是否在Redis中
	if token != nil && token.Raw != "" {
		nowUnix := time.Now().Unix()
		timer := time.Duration(token.Claims.(*customClaims).ExpiresAt-nowUnix) * time.Second

		return redisSync.Handler("blackListLock:"+crypto.Md5([]byte(token.Raw)), func() error {
			if isLogout {
				// 如果是 进行用户退出操作 的话，需要 当前时间 减去 黑名单宽限时间 作为缓存 value 值
				// tokenBlacklistGracePeriod 为黑名单宽限时间，设置一个黑名单宽限时间，目的就是避免并发请求中，刷新了token导致部分请求失败的情况
				// 这里 减去 黑名单宽限时间 是为了让提前结束宽限时间，因为用户退出登录，就可以直接并发请求给禁止掉
				nowUnix = nowUnix - tokenBlacklistGracePeriod - 1
			}

			// 将 token 剩余时间设置为缓存有效期，并将当前时间作为缓存 value 值
			if redisClient.SetNX(getBlackListKey(token.Raw), nowUnix, timer) != nil {
				return errors.New("加入黑名单失败~")
			}
			return nil
		}, false, redsync.WithExpiry(timer), redsync.WithTries(2))
	}
	return nil
}

// IsInBlacklist token 是否在黑名单中
func IsInBlacklist(tokenStr string) bool {
	if joinUnixStr := redisClient.Get(getBlackListKey(tokenStr)); joinUnixStr != "" {
		joinUnix, err := strconv.ParseInt(joinUnixStr, 10, 64)
		if err != nil {
			return false
		}

		// tokenBlacklistGracePeriod 为黑名单宽限时间，设置一个黑名单宽限时间，目的就是避免并发请求中，刷新了token导致部分请求失败的情况
		return (time.Now().Unix() - joinUnix) > tokenBlacklistGracePeriod
	}
	return false
}

// 获取移除token缓存 key
func getBlackListKey(tokenStr string) string {
	return "blackList:" + crypto.Md5([]byte(tokenStr))
}

// customClaims 自定义 Claims
type customClaims struct {
	Params string
	jwt.StandardClaims
}

type validTokenReturn struct {
	Params      string
	NewTokenStr string //新的token
	Token       *jwt.Token
}
